///////////////////////////////////////////////////////////////////////
//  File:      MinMaxShipCSharp.cs
//
//  Summary:   
//
//  Sample:    MinMaxShip
//
///////////////////////////////////////////////////////////////////////
//  This file is part of the Microsoft Commerce Server 2002 SDK
//
//  Copyright (C) 2002 Microsoft Corporation.  All rights reserved.
//
// This source code is intended only as a supplement to Microsoft
// Commerce Server 2002 and/or on-line documentation. See these other
// materials for detailed information regarding Microsoft code samples.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
///////////////////////////////////////////////////////////////////////

// MinMaxShipCSharp.cs modified by Chris Gruber September 2005 to
// incorporate UPS XML Service for Rate Checking
// I do not guarantee any of it and am posting this because I am a nice guy!
// Any code not written by me was found on the internet at freely available sites.

// A portion of the UPS parsing/request code if from John Qin and was found at
// http://dotnetjunkies.com/WebLog/feelexit/archive/2004/06/23/17364.aspx

// Go to UPS.COM and view their online tools for a better understanding.

// A couple brief notes:
// 1. We are always shipping from the same location... 
// those values for ship from address are hardcoded.
// 2. We use standard size boxes for shipping, also hardcoded.
// 3. To implement this in Business Desk, follow Microsofts instructions at
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/csvr2002/htm/cs_pp_buildingpipecomps_onaz.asp
//  (in MixMax_Ship.asp file, modify label for floor to be "UPS Service Code" and Percentage to be "Markup Prentage" or "Handling Percentage
// 4. If UPS service fails, shipping is calculated as a percentage of the total cost of the order.
// 5. I am on a time budget for this project so this is code is sloppy and not enough error checking is present,
// I strongly advise to add some more data integrity checks!

using System;
using System.Runtime.InteropServices;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Web;
using System.Xml;
using Microsoft.CommerceServer.Runtime;
using Microsoft.CommerceServer.Interop;
using Microsoft.CommerceServer.Interop.Orders;



namespace Commerce
{
	// This class demonstrates how to implement a Commerce Server pipeline
	// component using C#.


	// The MinMaxCSharp component is an Order Processing Pipeline (OPP)
	// component that extends the shipping component architecture of
	// Commerce Server 2002.
	
	public class MinMaxShipCSharp : IPipelineComponentAdmin,
		IPipelineComponent,
		IPipelineComponentDescription,
		IPersistDictionary
	{

		#region Variables

		// ProgID of Component - Keep this consistent with the real ProgID
		private const String ProgId = "Commerce.MinMaxShipCSharp";

		// Constant strings corresponding to dictionary keys
		private const String AddressesKey = "Addresses";
		private const String AdjustedPriceKey = "_cy_oadjust_adjustedprice";
		private const String BasketErrorsKey = "_Basket_Errors";
		private const String CountryCodeKey = "country_code";
		private const String DecimalPlacesKey = "_currency_decimal_places";
		private const String FloorKey = "floor";
		private const String ItemIndexesKey = "ItemIndexes";
		private const String ItemsKey = "Items";
		private const String MessageManagerKey = "MessageManager";
		private const String PercentageKey = "percentage";
		private const String ShipmentsToProcessKey = "shipments_to_process";
		private const String ShippingAddressIdKey = "shipping_address_id";
		private const String ShippingTotalKey = "_cy_shipping_total";
		
		//cgruber
		private const String PostalCodeKey = "postal_code";
		private const String ShipFromZip = "43228";
		private const String ShipFromCountry = "US";
		private const String ShippingMethodKey = "shipping_method_name";
		private const String WeightKey = "_product_weight";
		private const String QuantityKey = "quantity";

		// Value of a far away country to which this shipping component can't ship to
		private const String InvalidCountry = "FarAwayCountry";

		// The MessageManger error message indicating that we can't ship to the far away country
		private const String InvalidCountryMessage = "CantShipFarAway";

		// Default values for the minimum shipping price and maximum percentage of each item's cost
		private const decimal MinFloor = 03M;
		private const decimal MaxPercentage = 10M;

		// Status codes for pipeline components
		private const Int32 StatusSuccess = 1;	// success
		private const Int32 StatusWarning = 2;	// warning
		private const Int32 StatusError = 3;	// error

		private decimal Floor;			// the lower bound on the shipping cost
		private decimal Percentage;		// shipping cost as expressed by the percentage of an item's cost
		private bool isDirty;			// indicates that values of this component must be persisted

		#endregion

		#region UPSVariables
		//========================================
		// VISIT UPS.COM and register for their online services to obtain license, username, password
		string license = "XXXXXXXXXXXXXXXX";
		string userName = "XXXXXXXXXXXX";
		string passWord = "XXXXXX";
		//========================================
		//CHECKRATE VARIABLES
		string pickupCode = "01";  // UPS DAILY PICKUP
		string shipperZip = "43228";  // ALWAYS SAME SHIPPER LOCATION
		string shipperCountry = "US";
		string shipToCountry = "US";
		string shipFromZip = "43228"; // ALWAYS SHIPPING FROM SAME LOCATION
		string shipFromCountry = "US";
		string serviceCode = "";
		string packageCode = "02";  // USING OUR PACKAGING
		string measure = "IN";
		string weightCode = "LBS";
		string length = "19";  // PACKAGE DIMENSIONS
		string width = "12";
		string height = "9";

		string url = "https://www.ups.com/ups.app/xml/Rate";

		// xml format UPS expects...with placeholders for
		// formatting it later with correct user information
		string XmlRequest = @"<?xml version='1.0'?>
    <AccessRequest xml:lang='en-US'>
       <AccessLicenseNumber>{0}</AccessLicenseNumber>
       <UserId>{1}</UserId>
       <Password>{2}</Password>
    </AccessRequest>
    <?xml version='1.0' ?> 
	<RatingServiceSelectionRequest xml:lang='en-US'>
		<Request>
			<TransactionReference>
				<CustomerContext>Bare Bones Rate Request</CustomerContext> 
				<XpciVersion>1.0</XpciVersion> 
			</TransactionReference>
			<RequestAction>Rate</RequestAction> 
			<RequestOption>Rate</RequestOption> 
		</Request>
		<PickupType>
			<Code>{3}</Code> 
		</PickupType>
		<Shipment>
			<Shipper>
				<Address>
					<PostalCode>{4}</PostalCode> 
					<CountryCode>{5}</CountryCode> 
				</Address>
			</Shipper>
			<ShipTo>
				<Address>
					<PostalCode>{6}</PostalCode> 
					<CountryCode>{7}</CountryCode> 
				</Address>
			</ShipTo>
			<ShipFrom>
				<Address>
					<PostalCode>{8}</PostalCode> 
					<CountryCode>{9}</CountryCode> 
				</Address>
			</ShipFrom>
		<Service>
			<Code>{10}</Code> 
		</Service>
		<Package>
			<PackagingType>
				<Code>{11}</Code> 
			</PackagingType>
			<Dimensions>
				<UnitOfMeasurement>
					<Code>{12}</Code> 
				</UnitOfMeasurement>
				<Length>{13}</Length> 
				<Width>{14}</Width> 
				<Height>{15}</Height> 
			</Dimensions>
			<PackageWeight>
				<UnitOfMeasurement>
					<Code>{16}</Code> 
				</UnitOfMeasurement>
				<Weight>{17}</Weight> 
			</PackageWeight>
		</Package>
	</Shipment>
	</RatingServiceSelectionRequest>";

		#endregion

		#region MinMaxMethods

		// ************************************************************
		// ::Constructor - initialize instance member variables
		// ************************************************************
		public MinMaxShipCSharp()
		{
			isDirty = false;
			this.InitNew();
		}

		// ************************************************************
		// IPersistDictionary::GetProgID - return this components progId
		// ************************************************************
		public String GetProgID()
		{
			return ProgId;
		}

		// ************************************************************
		// IPersistDictionary::InitNew - Set the default values for the
		// minimum and maximum shipping rates
		// ************************************************************
		public void InitNew()
		{			
			Percentage = MaxPercentage;
			Floor = MinFloor;
			// UPS Shipping Service Codes:
			// 01 : Next Day Air
			// 02 : 2nd Day Air
			// 03 : UPS Ground
			// 12 : 3 Day Select
			// We are using Floor as the shipping service code which 
			// is passed in through the Business Desk
			serviceCode = System.Convert.ToString(Floor);
			serviceCode = serviceCode.Trim();
			// Add leading 0 to shipping service code if less than 10
			if( serviceCode.Length == 1 )
				serviceCode = "0" + serviceCode;
		}

		// ************************************************************
		// IPersistDictionary::IsDirty
		// ************************************************************
		public Int32 IsDirty()
		{
			return Convert.ToInt32(isDirty);
		}

		// ************************************************************
		// IPersistDictionary::Load - use the values within the
		// argument dictionary to populate internal values for min and
		// max shipping rates
		// ************************************************************
		public void Load(Object pDispDict)
		{
			Microsoft.CommerceServer.Runtime.IDictionary Dictionary = (Microsoft.CommerceServer.Runtime.IDictionary) pDispDict;

			Floor = Convert.ToDecimal(Dictionary[FloorKey]);
			Percentage = Convert.ToDecimal(Dictionary[PercentageKey]);
			// UPS Shipping Service Codes:
			// 01 : Next Day Air
			// 02 : 2nd Day Air
			// 03 : UPS Ground
			// 12 : 3 Day Select
			// We are using Floor as the shipping service code which 
			// is passed in through the Business Desk
			serviceCode = System.Convert.ToString(Floor);
			serviceCode = serviceCode.Trim();
			// Add leading 0 to shipping service code if less than 10
			if( serviceCode.Length == 1 )
				serviceCode = "0" + serviceCode;
		}

		// ************************************************************
		// IPersistDictionary::Save - save internal min/max shipping 
		// rate values to the the argument dictionary
		// ************************************************************
		public void Save(Object pDispDict, Int32 fSameAsLoad)
		{
			Microsoft.CommerceServer.Runtime.IDictionary Dictionary = (Microsoft.CommerceServer.Runtime.IDictionary) pDispDict;

			Dictionary[FloorKey] = Floor;
			Dictionary[PercentageKey] = Percentage;
		}

		// ************************************************************
		// IPipelinecomponent::Execute - main function for this component.
		// For each shipment cycle through the line items and determine
		// if a minimum value or the summation of a fixed percentage of 
		// each item should be used as the shipping rate.
		// ************************************************************
		public Int32 Execute(Object pdispOrder, Object pdispContext, Int32 Flags)
		{
			Microsoft.CommerceServer.Runtime.IDictionary Order = null;		// OrderForm dictionary representing the basket
			Microsoft.CommerceServer.Runtime.IDictionary Context = null;		// Context dictionary
			ISimpleList ShipmentsToProcess = null;	// Simplelist of shipments in this order
			ISimpleList Items = null;		// Simplelist of items to process for each shipment
			Microsoft.CommerceServer.Runtime.IDictionary Addresses = null;		// Dictionary object representing a shipping address
			IMessageManager MessageManager = null;	// MessageManager used to return pipeline errors
			Object ErrorMessage = null;		// Error message returned from pipeline component
			Int32 ReturnValue = StatusSuccess;	// Status level for component execution

			try
			{
				// initialize the Order and Context dictionaries
				Order = (Microsoft.CommerceServer.Runtime.IDictionary) pdispOrder;
				Context = (Microsoft.CommerceServer.Runtime.IDictionary) pdispContext;
				
	
				// initialize the Addresses data structure, Orderform.Items, and MessageManager objects
				Addresses = (Microsoft.CommerceServer.Runtime.IDictionary)Order[AddressesKey];
				Items = (ISimpleList)Order[ItemsKey];
				MessageManager = (IMessageManager) Context[MessageManagerKey];

				// retrive the shipments that must be processed
				ShipmentsToProcess = (ISimpleList)Context[ShipmentsToProcessKey];
		
				// for each shipment retrieve the line items in the shipment
				// and determine which shipping rate to use based on the
				// pricing of the line items
				foreach (Microsoft.CommerceServer.Runtime.IDictionary Shipment in ShipmentsToProcess)
				{
					decimal ShipmentItemTotal = 0;	// total cost of goods in this shipment
					decimal ShipmentWeightTotal = 0; // total weight of goods in this shipment cgruber
					Microsoft.CommerceServer.Runtime.IDictionary Item;		// represents an item in this shipment

					// iterate over all the items and sum up the shipment item total
					foreach (Int32 ItemIndex in (ISimpleList)Shipment[ItemIndexesKey])
					{
						Item = (Microsoft.CommerceServer.Runtime.IDictionary)Items[ItemIndex];				
						ShipmentWeightTotal += Convert.ToDecimal(Item[WeightKey]) * Convert.ToDecimal(Item[QuantityKey]);
						ShipmentItemTotal += Convert.ToDecimal(Item[AdjustedPriceKey]);
					}

					String ShippingAddressId;	// shipping address Id
					String Country;			// destination country for shipment
					String PostalCode;		// destination address

					Microsoft.CommerceServer.Runtime.IDictionary Address;		// Address dictionary

					// examine address information
					ShippingAddressId = Shipment[ShippingAddressIdKey].ToString();
					Address = (Microsoft.CommerceServer.Runtime.IDictionary) Addresses[ShippingAddressId];

					// retrieve the destination country of the shipment
					Country = Address[CountryCodeKey].ToString();
					// retrieve the destination address for UPS rates
					PostalCode = Address[PostalCodeKey].ToString();
	
					// cgruber
					// UPS can only quote on orders of 150 pounds or less, split up weight if over 150
					decimal ShipmentShippingPrice = 0;
					decimal costPer150LBS = 0;
					decimal quantity150LBS = 0;
			

					if ( ShipmentWeightTotal <= 150 )
						ShipmentShippingPrice = GetShippingCost(PostalCode, ShipmentWeightTotal);
					else
					{
						decimal leftover = decimal.Remainder( ShipmentWeightTotal ,(decimal) 150 );

						// Don't want to hit XML service for weight/150 times ... lets just do it once
						costPer150LBS = GetShippingCost(PostalCode, (decimal) 150);	
						
						quantity150LBS = decimal.Divide( ShipmentWeightTotal, (decimal) 150 );
						
						ShipmentShippingPrice = decimal.Round(decimal.Multiply( costPer150LBS, quantity150LBS), 2);
						// What is left after weight % 150
						if ( leftover > 0 )
							ShipmentShippingPrice += GetShippingCost(PostalCode, leftover);
					}

					//Add handling.  Handling fee is a percentage of the UPS rate
					if ( Percentage <= 0 )
						ShipmentShippingPrice += ( decimal.Multiply(ShipmentShippingPrice, decimal.Divide(Percentage, 100) ));
					
					Shipment[ShippingTotalKey] = RoundCurrency(Order, ShipmentShippingPrice);
					
				}
				
			}
				// an error has occured. Add the error message to the basket errors and 
				// set the "error" as the pipeline component error level
			catch (Exception e)
			{
				ErrorMessage = e.Message;
				((ISimpleList)Order[BasketErrorsKey]).Add(ref ErrorMessage);
				return StatusError;
			}

			return ReturnValue;
		}


		// ************************************************************
		// ::RoundCurrency - Internal helper function to round currency values 
		// based on the degree of precision specified by the 
		// _currency_decimal_places key in the OrderForm dictionary
		// ************************************************************
		private decimal RoundCurrency(Microsoft.CommerceServer.Runtime.IDictionary Order, decimal Currency)
		{
			// retrieve the degree of precision from the OrderForm dictionary
			Object DecimalPlaces = Order[DecimalPlacesKey];

			// assert that a non null value has been retrieved
			if (DecimalPlaces != DBNull.Value)
			{
				// represents the precision level specified
				Int32 Precision = Convert.ToInt32(DecimalPlaces);
				
				// if the precision level is greater than 0 then round
				// the currency value to the desired number of significant
				// digits. Otherwise (negative precision level) divide by
				// 10^(precision level), round dropping digits to the right
				// of the decimal, and then multiply by 10*(precision level)
				//
				// For example if precision == -2 and currency == 1234.56 then
				// this algorithm will return 1200
				if (Precision > 0)
				{
					return Convert.ToDecimal(Math.Round(Currency, Precision));
				}
				else
				{
					Int32 f = 1;
					while (Precision <0)
					{
						f *= 10;
						Precision +=1;
					}
					Currency = Currency / f;
					Currency = Math.Round(Currency);
					Currency = Currency * f;
				}
			}
			return Currency;
		}

		// ************************************************************
		// IPipelinecomponent::EnableDesign
		// ************************************************************
		public void EnableDesign(Int32 fEnable) {}


		// ************************************************************
		// IPipelineComponentAdmin::GetConfigData - return the dictionary
		// object holding the configuration values for this component
		// ************************************************************
		public Object GetConfigData()
		{
			Object Dictionary = new DictionaryClass();
			this.Save(Dictionary, Convert.ToInt32(false));
			return Dictionary;
		}

		// ************************************************************
		// IPipelineComponentAdmin::SetConfigData - load the configuration
		// values for this component based on the passed in dictionary
		// ************************************************************
		public void SetConfigData(Object pdispDict)
		{
			Microsoft.CommerceServer.Runtime.IDictionary Dictionary = (Microsoft.CommerceServer.Runtime.IDictionary) pdispDict;
			this.Load(Dictionary);
		}


		// ************************************************************
		// IPipelineComponentDescription::ContextValuesRead 
		// Returns an array of strings containing the ContextValuesRead
		// ************************************************************
		public System.Object ContextValuesRead()	
		{	
			object[] contextValuesRead = new object[0];	
			return contextValuesRead;
		}

		// ************************************************************
		// IPipelineComponentDescription::ValuesRead 
		// Returns an array of strings containing the ContextValuesRead
		// ************************************************************
		public System.Object ValuesRead()			
		{	
			object[] valuesRead = new object[4];
			valuesRead[0] = "shipments_to_process";
			valuesRead[1] = "item._cy_oadjust_adjustedprice";
			valuesRead[2] = "item._product_weight";
			valuesRead[3] = "shipments._cy_shipping_total";	
			return valuesRead;
		}

		// ************************************************************
		// IPipelineComponentDescription::ValuesWritten 
		// Returns an array of strings containing the ValuesWritten
		// ************************************************************
		public System.Object ValuesWritten()		
		{	
			object[] valuesWritten = new object[1];
			valuesWritten[0] = "shipments._cy_shipping_total";	
			return valuesWritten;
		}
	
		#endregion

		#region UPSMethods

		//Function which returns rate data.
		private decimal GetShippingCost(string shipToZip, decimal shippingWeight) 		
		{
			string weight = shippingWeight.ToString();
			// format xml data
			string XmlPostData = String.Format(XmlRequest,license,userName,passWord, pickupCode, shipperZip, shipperCountry,
				shipToZip, shipToCountry, shipFromZip, shipFromCountry, serviceCode, packageCode, measure,
				length, width, height, weightCode, weight);
			

			// get byte array for sending
			ASCIIEncoding encodedData=new ASCIIEncoding();
			byte[]  byteArray=encodedData.GetBytes(XmlPostData);

			// open up da site
			HttpWebRequest wr = (HttpWebRequest) WebRequest.Create(url);
			wr.Method = "POST";
			wr.KeepAlive = false;
			wr.UserAgent = "Patoooey";
			wr.ContentType = "application/x-www-form-urlencoded";
			wr.ContentLength = XmlPostData.Length;

			// send xml data
			Stream SendStream = wr.GetRequestStream();
			SendStream.Write(byteArray,0,byteArray.Length);
			SendStream.Close();

			// get da response
			HttpWebResponse WebResp = (HttpWebResponse) wr.GetResponse();
			string res = "";
			string shippingCostString = "";
			try
			{
				using (StreamReader sr = new StreamReader(WebResp.GetResponseStream()) )
				{
					res = sr.ReadToEnd();
					sr.Close();
				}

				shippingCostString = parseRetVal(res);
			}
			catch
			{
				//If fails, shipping will be calculated as 10% of total cost
				return (decimal) -1;
			}
			WebResp.Close();
			return System.Convert.ToDecimal(shippingCostString);
			

		}
		
		
		private string parseRetVal(string xmlData)
		{
			string retVal = "";
			XmlDocument xDoc = new XmlDocument();
			xDoc.LoadXml(xmlData);
			XmlNodeList errors = xDoc.SelectNodes("RatingServiceSelectionResponse/Response");
			string error = errors.Item(0).SelectSingleNode("ResponseStatusDescription").InnerText;

			retVal += "<b>Status = " + error + "</b><br /><br />";

			XmlNodeList locations = xDoc.SelectNodes("RatingServiceSelectionResponse/RatedShipment");

			string totalCharges = "";
			foreach(XmlNode location in locations)
			{
				string code = location.SelectSingleNode("Service/Code").InnerText;
				//UNKNOWN WHY FOLLOWING LINE CAUSES ERROR
				//string warning = location.SelectSingleNode("RatedShipmentWarning").InnerText;
				string unitMeasure = location.SelectSingleNode("BillingWeight/UnitOfMeasurement").InnerText;
				string weight = location.SelectSingleNode("BillingWeight/Weight").InnerText;			
				string transChargeCurr = location.SelectSingleNode("TransportationCharges/CurrencyCode").InnerText;
				string charges = location.SelectSingleNode("TransportationCharges/MonetaryValue").InnerText;
				string serviceCurr = location.SelectSingleNode("ServiceOptionsCharges/CurrencyCode").InnerText;
				string serviceCharges = location.SelectSingleNode("ServiceOptionsCharges/MonetaryValue").InnerText;
				string totalCurr = location.SelectSingleNode("TotalCharges/CurrencyCode").InnerText;
				totalCharges = location.SelectSingleNode("TotalCharges/MonetaryValue").InnerText;
				string days = location.SelectSingleNode("GuaranteedDaysToDelivery").InnerText;	
				string time = location.SelectSingleNode("ScheduledDeliveryTime").InnerText;			
				string transCurr = location.SelectSingleNode("RatedPackage/TransportationCharges/CurrencyCode").InnerText;
				string transCharges = location.SelectSingleNode("RatedPackage/TransportationCharges/MonetaryValue").InnerText;
				string accessCurr = location.SelectSingleNode("RatedPackage/ServiceOptionsCharges/CurrencyCode").InnerText;
				string accessCharges = location.SelectSingleNode("RatedPackage/ServiceOptionsCharges/MonetaryValue").InnerText;
				string ratedTotalCharges = location.SelectSingleNode("RatedPackage/TotalCharges/MonetaryValue").InnerText;	
				string ratedCurr = location.SelectSingleNode("RatedPackage/TotalCharges/CurrencyCode").InnerText;
				string ratedWeight = location.SelectSingleNode("RatedPackage/Weight").InnerText;
				string billableMeasure = location.SelectSingleNode("RatedPackage/BillingWeight/UnitOfMeasurement/Code").InnerText;	
				string billableWeight = location.SelectSingleNode("RatedPackage/BillingWeight/Weight").InnerText;
	
				retVal += "UPS Service Code: " + code + "<br />Weight: " + weight + unitMeasure + 
					"<br />Transportation Charges: " + charges + transChargeCurr + "<br />Service Charge: "
					+ serviceCharges + serviceCurr + "<br />Total Charges: " + totalCharges + totalCurr + 
					"<br />Guaranteed Days to Delivery: " + days + "<br />Before: " + time + "<br /><br />";
				
				retVal += "Rated Package<br />Transportation Charges: " + transCharges + transCurr + "<br />" +
					"Service Option Charges: " + accessCharges + accessCurr + "<br />Total Charges: " + ratedTotalCharges + ratedCurr + "<br />" +
					"Weight: " + ratedWeight + billableMeasure + "<br />Billing Weight: " + billableWeight + billableMeasure + "<br />";
			}
			return totalCharges;
		}

		#endregion

	}
}